package com.frostwire.mp3;

import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public abstract class AbstractID3v2Tag implements ID3v2 {
    public static final String ID_IMAGE = "APIC";
    public static final String ID_ENCODER = "TENC";
    public static final String ID_URL = "WXXX";
    public static final String ID_COPYRIGHT = "TCOP";
    public static final String ID_ORIGINAL_ARTIST = "TOPE";
    public static final String ID_COMPOSER = "TCOM";
    public static final String ID_COMMENT = "COMM";
    public static final String ID_GENRE = "TCON";
    public static final String ID_YEAR = "TYER";
    public static final String ID_ALBUM = "TALB";
    public static final String ID_TITLE = "TIT2";
    public static final String ID_ARTIST = "TPE1";
    public static final String ID_TRACK = "TRCK";
    public static final String ID_IMAGE_OBSELETE = "PIC";
    public static final String ID_ENCODER_OBSELETE = "TEN";
    public static final String ID_URL_OBSELETE = "WXX";
    public static final String ID_COPYRIGHT_OBSELETE = "TCR";
    public static final String ID_ORIGINAL_ARTIST_OBSELETE = "TOA";
    public static final String ID_COMPOSER_OBSELETE = "TCM";
    public static final String ID_COMMENT_OBSELETE = "COM";
    public static final String ID_GENRE_OBSELETE = "TCO";
    public static final String ID_YEAR_OBSELETE = "TYE";
    public static final String ID_ALBUM_OBSELETE = "TAL";
    public static final String ID_TITLE_OBSELETE = "TT2";
    public static final String ID_ARTIST_OBSELETE = "TP1";
    public static final String ID_TRACK_OBSELETE = "TRK";
    protected static final String TAG = "ID3";
    protected static final String FOOTER_TAG = "3DI";
    protected static final int HEADER_LENGTH = 10;
    protected static final int FOOTER_LENGTH = 10;
    protected static final int MAJOR_VERSION_OFFSET = 3;
    protected static final int MINOR_VERSION_OFFSET = 4;
    protected static final int FLAGS_OFFSET = 5;
    protected static final int DATA_LENGTH_OFFSET = 6;
    protected static final int FOOTER_BIT = 4;
    protected static final int EXPERIMENTAL_BIT = 5;
    protected static final int EXTENDED_HEADER_BIT = 6;
    protected static final int COMPRESSION_BIT = 6;
    protected static final int UNSYNCHRONISATION_BIT = 7;
    protected static final int PADDING_LENGTH = 256;
    private static final String ITUNES_COMMENT_DESCRIPTION = "iTunNORM";
    private final Map<String, ID3v2FrameSet> frameSets;
    protected boolean unsynchronisation = false;
    protected boolean extendedHeader = false;
    protected boolean experimental = false;
    protected boolean footer = false;
    protected boolean compression = false;
    protected boolean padding = false;
    protected String version = null;
    private int dataLength = 0;
    private int extendedHeaderLength;
    private byte[] extendedHeaderData;
    private boolean obseleteFormat = false;

    public AbstractID3v2Tag() {
        frameSets = new TreeMap<>();
    }

    public AbstractID3v2Tag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {
        this(bytes, false);
    }

    public AbstractID3v2Tag(byte[] bytes, boolean obseleteFormat) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {
        frameSets = new TreeMap<>();
        this.obseleteFormat = obseleteFormat;
        unpackTag(bytes);
    }

    private void unpackTag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {
        ID3v2TagFactory.sanityCheckTag(bytes);
        int offset = unpackHeader(bytes);
        try {
            if (extendedHeader) {
                offset = unpackExtendedHeader(bytes, offset);
            }
            int framesLength = dataLength;
            if (footer) framesLength -= 10;
            offset = unpackFrames(bytes, offset, framesLength);
            if (footer) {
                offset = unpackFooter(bytes, dataLength);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new InvalidDataException("Premature end of tag", e);
        }
    }

    private int unpackHeader(byte[] bytes) throws UnsupportedTagException, InvalidDataException {
        int majorVersion = bytes[MAJOR_VERSION_OFFSET];
        int minorVersion = bytes[MINOR_VERSION_OFFSET];
        version = majorVersion + "." + minorVersion;
        if (majorVersion != 2 && majorVersion != 3 && majorVersion != 4) {
            throw new UnsupportedTagException("Unsupported version " + version);
        }
        unpackFlags(bytes);
        if ((bytes[FLAGS_OFFSET] & 0x0F) != 0) throw new UnsupportedTagException("Unrecognised bits in header");
        dataLength = BufferTools.unpackSynchsafeInteger(bytes[DATA_LENGTH_OFFSET], bytes[DATA_LENGTH_OFFSET + 1], bytes[DATA_LENGTH_OFFSET + 2], bytes[DATA_LENGTH_OFFSET + 3]);
        if (dataLength < 1) throw new InvalidDataException("Zero size tag");
        return HEADER_LENGTH;
    }

    protected abstract void unpackFlags(byte[] bytes);

    private int unpackExtendedHeader(byte[] bytes, int offset) {
        extendedHeaderLength = BufferTools.unpackSynchsafeInteger(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]) + 4;
        extendedHeaderData = BufferTools.copyBuffer(bytes, offset + 4, extendedHeaderLength);
        return extendedHeaderLength;
    }

    protected int unpackFrames(byte[] bytes, int offset, int framesLength) {
        int currentOffset = offset;
        while (currentOffset <= framesLength) {
            ID3v2Frame frame;
            try {
                frame = createFrame(bytes, currentOffset);
                addFrame(frame, false);
                currentOffset += frame.getLength();
            } catch (InvalidDataException e) {
                break;
            }
        }
        return currentOffset;
    }

    private void addFrame(ID3v2Frame frame, boolean replace) {
        ID3v2FrameSet frameSet = frameSets.get(frame.getId());
        if (frameSet == null) {
            frameSet = new ID3v2FrameSet(frame.getId());
            frameSet.addFrame(frame);
            frameSets.put(frame.getId(), frameSet);
        } else if (replace) {
            frameSet.clear();
            frameSet.addFrame(frame);
        } else {
            frameSet.addFrame(frame);
        }
    }

    protected ID3v2Frame createFrame(byte[] bytes, int currentOffset) throws InvalidDataException {
        if (obseleteFormat) return new ID3v2ObseleteFrame(bytes, currentOffset);
        return new ID3v2Frame(bytes, currentOffset);
    }

    protected ID3v2Frame createFrame(String id, byte[] data) {
        if (obseleteFormat) return new ID3v2ObseleteFrame(id, data);
        else return new ID3v2Frame(id, data);
    }

    private int unpackFooter(byte[] bytes, int offset) throws InvalidDataException {
        if (!FOOTER_TAG.equals(BufferTools.byteBufferToString(bytes, offset, FOOTER_TAG.length()))) {
            throw new InvalidDataException("Invalid footer");
        }
        return FOOTER_LENGTH;
    }

    public byte[] toBytes() throws NotSupportedException {
        byte[] bytes = new byte[getLength()];
        packTag(bytes);
        return bytes;
    }

    public void packTag(byte[] bytes) throws NotSupportedException {
        int offset = packHeader(bytes, 0);
        if (extendedHeader) {
            offset = packExtendedHeader(bytes, offset);
        }
        offset = packFrames(bytes, offset);
        if (footer) {
            offset = packFooter(bytes, dataLength);
        }
    }

    private int packHeader(byte[] bytes, int offset) {
        BufferTools.stringIntoByteBuffer(TAG, 0, TAG.length(), bytes, offset);
        String[] s = version.split("\\.");
        if (s.length > 0) {
            byte majorVersion = Byte.parseByte(s[0]);
            bytes[offset + MAJOR_VERSION_OFFSET] = majorVersion;
        }
        if (s.length > 1) {
            byte minorVersion = Byte.parseByte(s[1]);
            bytes[offset + MINOR_VERSION_OFFSET] = minorVersion;
        }
        packFlags(bytes, offset);
        BufferTools.packSynchsafeInteger(getDataLength(), bytes, offset + DATA_LENGTH_OFFSET);
        return offset + HEADER_LENGTH;
    }

    protected abstract void packFlags(byte[] bytes, int i);

    private int packExtendedHeader(byte[] bytes, int offset) {
        BufferTools.packSynchsafeInteger(extendedHeaderLength, bytes, offset);
        BufferTools.copyIntoByteBuffer(extendedHeaderData, 0, extendedHeaderData.length, bytes, offset + 4);
        return offset + 4 + extendedHeaderData.length;
    }

    public int packFrames(byte[] bytes, int offset) throws NotSupportedException {
        int newOffset = packSpecifiedFrames(bytes, offset, null, "APIC");
        newOffset = packSpecifiedFrames(bytes, newOffset, "APIC", null);
        return newOffset;
    }

    private int packSpecifiedFrames(byte[] bytes, int offset, String onlyId, String notId) throws NotSupportedException {
        Iterator<ID3v2FrameSet> setIterator = frameSets.values().iterator();
        while (setIterator.hasNext()) {
            ID3v2FrameSet frameSet = setIterator.next();
            if ((onlyId == null || onlyId.equals(frameSet.getId())) && (notId == null || !notId.equals(frameSet.getId()))) {
                Iterator<ID3v2Frame> frameIterator = frameSet.getFrames().iterator();
                while (frameIterator.hasNext()) {
                    ID3v2Frame frame = (ID3v2Frame) frameIterator.next();
                    if (frame.getDataLength() > 0) {
                        byte[] frameData = frame.toBytes();
                        BufferTools.copyIntoByteBuffer(frameData, 0, frameData.length, bytes, offset);
                        offset += frameData.length;
                    }
                }
            }
        }
        return offset;
    }

    private int packFooter(byte[] bytes, int offset) {
        BufferTools.stringIntoByteBuffer(FOOTER_TAG, 0, FOOTER_TAG.length(), bytes, offset);
        String[] s = version.split(".");
        if (s.length > 0) {
            byte majorVersion = Byte.parseByte(s[0]);
            bytes[offset + MAJOR_VERSION_OFFSET] = majorVersion;
        }
        if (s.length > 1) {
            byte minorVersion = Byte.parseByte(s[0]);
            bytes[offset + MINOR_VERSION_OFFSET] = minorVersion;
        }
        packFlags(bytes, offset);
        BufferTools.packSynchsafeInteger(getDataLength(), bytes, offset + DATA_LENGTH_OFFSET);
        return offset + FOOTER_LENGTH;
    }

    private int calculateDataLength() {
        int length = 0;
        if (extendedHeader) length += extendedHeaderLength;
        if (footer) length += FOOTER_LENGTH;
        else if (padding) length += PADDING_LENGTH;
        Iterator<ID3v2FrameSet> setIterator = frameSets.values().iterator();
        while (setIterator.hasNext()) {
            ID3v2FrameSet frameSet = setIterator.next();
            Iterator<ID3v2Frame> frameIterator = frameSet.getFrames().iterator();
            while (frameIterator.hasNext()) {
                ID3v2Frame frame = (ID3v2Frame) frameIterator.next();
                length += frame.getLength();
            }
        }
        return length;
    }

    protected boolean useFrameUnsynchronisation() {
        return false;
    }

    public String getVersion() {
        return version;
    }

    private void invalidateDataLength() {
        dataLength = 0;
    }

    public int getDataLength() {
        if (dataLength == 0) {
            dataLength = calculateDataLength();
        }
        return dataLength;
    }

    public int getLength() {
        return getDataLength() + HEADER_LENGTH;
    }

    public Map<String, ID3v2FrameSet> getFrameSets() {
        return frameSets;
    }

    public boolean getPadding() {
        return padding;
    }

    public void setPadding(boolean padding) {
        if (this.padding != padding) {
            invalidateDataLength();
            this.padding = padding;
        }
    }

    public boolean hasFooter() {
        return footer;
    }

    public void setFooter(boolean footer) {
        if (this.footer != footer) {
            invalidateDataLength();
            this.footer = footer;
        }
    }

    public boolean hasUnsynchronisation() {
        return unsynchronisation;
    }

    public void setUnsynchronisation(boolean unsynchronisation) {
        if (this.unsynchronisation != unsynchronisation) {
            invalidateDataLength();
            this.unsynchronisation = unsynchronisation;
        }
    }

    public boolean getObseleteFormat() {
        return obseleteFormat;
    }

    public String getTrack() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_TRACK_OBSELETE);
        else frameData = extractTextFrameData(ID_TRACK);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setTrack(String track) {
        if (track != null && track.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, track));
            addFrame(createFrame(ID_TRACK, frameData.toBytes()), true);
        }
    }

    public String getArtist() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_ARTIST_OBSELETE);
        else frameData = extractTextFrameData(ID_ARTIST);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setArtist(String artist) {
        if (artist != null && artist.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, artist));
            addFrame(createFrame(ID_ARTIST, frameData.toBytes()), true);
        }
    }

    public String getTitle() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_TITLE_OBSELETE);
        else frameData = extractTextFrameData(ID_TITLE);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setTitle(String title) {
        if (title != null && title.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, title));
            addFrame(createFrame(ID_TITLE, frameData.toBytes()), true);
        }
    }

    public String getAlbum() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_ALBUM_OBSELETE);
        else frameData = extractTextFrameData(ID_ALBUM);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setAlbum(String album) {
        if (album != null && album.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, album));
            addFrame(createFrame(ID_ALBUM, frameData.toBytes()), true);
        }
    }

    public String getYear() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_YEAR_OBSELETE);
        else frameData = extractTextFrameData(ID_YEAR);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setYear(String year) {
        if (year != null && year.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, year));
            addFrame(createFrame(ID_YEAR, frameData.toBytes()), true);
        }
    }

    public int getGenre() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_GENRE_OBSELETE);
        else frameData = extractTextFrameData(ID_GENRE);
        if (frameData == null || frameData.getText() == null) return -1;
        String text = frameData.getText().toString();
        if (text == null || text.length() == 0) return -1;
        try {
            return extractGenreNumber(text);
        } catch (NumberFormatException e) {
            String description = extractGenreDescription(text);
            if (description != null && description.length() > 0) {
                for (int i = 0; i < ID3v1Genres.GENRES.length; i++) {
                    if (ID3v1Genres.GENRES[i].compareToIgnoreCase(description) == 0) return i;
                }
            }
        }
        return -1;
    }

    public void setGenre(int genre) {
        if (genre >= 0) {
            invalidateDataLength();
            String genreDescription;
            try {
                genreDescription = ID3v1Genres.GENRES[genre];
            } catch (ArrayIndexOutOfBoundsException e) {
                genreDescription = "";
            }
            String combinedGenre = "(" + genre + ")" + genreDescription;
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, combinedGenre));
            addFrame(createFrame(ID_GENRE, frameData.toBytes()), true);
        }
    }

    public String getGenreDescription() {
        int genreNum = getGenre();
        if (genreNum >= 0) {
            try {
                return ID3v1Genres.GENRES[genreNum];
            } catch (ArrayIndexOutOfBoundsException e) {
                return null;
            }
        }
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_GENRE_OBSELETE);
        else frameData = extractTextFrameData(ID_GENRE);
        if (frameData != null && frameData.getText() != null) {
            String text = frameData.getText().toString();
            if (text != null && text.length() > 0) {
                String description = extractGenreDescription(text);
                if (description != null & description.length() > 0) {
                    return description;
                }
            }
        }
        return null;
    }

    protected int extractGenreNumber(String genreValue) throws NumberFormatException {
        String value = genreValue.trim();
        if (value.length() > 0) {
            if (value.charAt(0) == '(') {
                int pos = value.indexOf(')');
                if (pos > 0) {
                    return Integer.parseInt(value.substring(1, pos));
                }
            }
        }
        return Integer.parseInt(value);
    }

    protected String extractGenreDescription(String genreValue) throws NumberFormatException {
        String value = genreValue.trim();
        if (value.length() > 0) {
            if (value.charAt(0) == '(') {
                int pos = value.indexOf(')');
                if (pos > 0) {
                    return value.substring(pos + 1);
                }
            }
            return value;
        }
        return null;
    }

    public String getComment() {
        ID3v2CommentFrameData frameData;
        if (obseleteFormat) frameData = extractCommentFrameData(ID_COMMENT_OBSELETE, false);
        else frameData = extractCommentFrameData(ID_COMMENT, false);
        if (frameData != null && frameData.getComment() != null) return frameData.getComment().toString();
        return null;
    }

    public void setComment(String comment) {
        if (comment != null && comment.length() > 0) {
            invalidateDataLength();
            ID3v2CommentFrameData frameData = new ID3v2CommentFrameData(useFrameUnsynchronisation(), "eng", null, new EncodedText((byte) 0, comment));
            addFrame(createFrame(ID_COMMENT, frameData.toBytes()), true);
        }
    }

    public String getItunesComment() {
        ID3v2CommentFrameData frameData;
        if (obseleteFormat) frameData = extractCommentFrameData(ID_COMMENT_OBSELETE, true);
        else frameData = extractCommentFrameData(ID_COMMENT, true);
        if (frameData != null && frameData.getComment() != null) return frameData.getComment().toString();
        return null;
    }

    public void setItunesComment(String itunesComment) {
        if (itunesComment != null && itunesComment.length() > 0) {
            invalidateDataLength();
            ID3v2CommentFrameData frameData = new ID3v2CommentFrameData(useFrameUnsynchronisation(), ITUNES_COMMENT_DESCRIPTION, null, new EncodedText((byte) 0, itunesComment));
            addFrame(createFrame(ID_COMMENT, frameData.toBytes()), true);
        }
    }

    public String getComposer() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_COMPOSER_OBSELETE);
        else frameData = extractTextFrameData(ID_COMPOSER);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setComposer(String composer) {
        if (composer != null && composer.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, composer));
            addFrame(createFrame(ID_COMPOSER, frameData.toBytes()), true);
        }
    }

    public String getOriginalArtist() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_ORIGINAL_ARTIST_OBSELETE);
        else frameData = extractTextFrameData(ID_ORIGINAL_ARTIST);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setOriginalArtist(String originalArtist) {
        if (originalArtist != null && originalArtist.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, originalArtist));
            addFrame(createFrame(ID_ORIGINAL_ARTIST, frameData.toBytes()), true);
        }
    }

    public String getCopyright() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_COPYRIGHT_OBSELETE);
        else frameData = extractTextFrameData(ID_COPYRIGHT);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setCopyright(String copyright) {
        if (copyright != null && copyright.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, copyright));
            addFrame(createFrame(ID_COPYRIGHT, frameData.toBytes()), true);
        }
    }

    public String getUrl() {
        ID3v2UrlFrameData frameData;
        if (obseleteFormat) frameData = extractUrlFrameData(ID_URL_OBSELETE);
        else frameData = extractUrlFrameData(ID_URL);
        if (frameData != null) return frameData.getUrl();
        return null;
    }

    public void setUrl(String url) {
        if (url != null && url.length() > 0) {
            invalidateDataLength();
            ID3v2UrlFrameData frameData = new ID3v2UrlFrameData(useFrameUnsynchronisation(), null, url);
            addFrame(createFrame(ID_URL, frameData.toBytes()), true);
        }
    }

    public String getEncoder() {
        ID3v2TextFrameData frameData;
        if (obseleteFormat) frameData = extractTextFrameData(ID_ENCODER_OBSELETE);
        else frameData = extractTextFrameData(ID_ENCODER);
        if (frameData != null && frameData.getText() != null) return frameData.getText().toString();
        return null;
    }

    public void setEncoder(String encoder) {
        if (encoder != null && encoder.length() > 0) {
            invalidateDataLength();
            ID3v2TextFrameData frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), new EncodedText((byte) 0, encoder));
            addFrame(createFrame(ID_ENCODER, frameData.toBytes()), true);
        }
    }

    public byte[] getAlbumImage() {
        ID3v2PictureFrameData frameData;
        if (obseleteFormat) frameData = createPictureFrameData(ID_IMAGE_OBSELETE);
        else frameData = createPictureFrameData(ID_IMAGE);
        if (frameData != null) return frameData.getImageData();
        return null;
    }

    public void setAlbumImage(byte[] albumImage, String mimeType) {
        if (albumImage != null && albumImage.length > 0 && mimeType != null && mimeType.length() > 0) {
            invalidateDataLength();
            ID3v2PictureFrameData frameData = new ID3v2PictureFrameData(useFrameUnsynchronisation(), mimeType, (byte) 0, null, albumImage);
            addFrame(createFrame(ID_IMAGE, frameData.toBytes()), true);
        }
    }

    public String getAlbumImageMimeType() {
        ID3v2PictureFrameData frameData;
        if (obseleteFormat) frameData = createPictureFrameData(ID_IMAGE_OBSELETE);
        else frameData = createPictureFrameData(ID_IMAGE);
        if (frameData != null && frameData.getMimeType() != null) return frameData.getMimeType();
        return null;
    }

    public void clearFrameSet(String id) {
        if (frameSets.remove(id) != null) {
            invalidateDataLength();
        }
    }

    private ID3v2TextFrameData extractTextFrameData(String id) {
        ID3v2FrameSet frameSet = frameSets.get(id);
        if (frameSet != null) {
            ID3v2Frame frame = frameSet.getFrames().get(0);
            ID3v2TextFrameData frameData;
            try {
                frameData = new ID3v2TextFrameData(useFrameUnsynchronisation(), frame.getData());
                return frameData;
            } catch (InvalidDataException e) {
                // do nothing
            }
        }
        return null;
    }

    private ID3v2UrlFrameData extractUrlFrameData(String id) {
        ID3v2FrameSet frameSet = frameSets.get(id);
        if (frameSet != null) {
            ID3v2Frame frame = frameSet.getFrames().get(0);
            ID3v2UrlFrameData frameData;
            try {
                frameData = new ID3v2UrlFrameData(useFrameUnsynchronisation(), frame.getData());
                return frameData;
            } catch (InvalidDataException e) {
                // do nothing
            }
        }
        return null;
    }

    private ID3v2CommentFrameData extractCommentFrameData(String id, boolean itunes) {
        ID3v2FrameSet frameSet = frameSets.get(id);
        if (frameSet != null) {
            Iterator<ID3v2Frame> iterator = frameSet.getFrames().iterator();
            while (iterator.hasNext()) {
                ID3v2Frame frame = iterator.next();
                ID3v2CommentFrameData frameData;
                try {
                    frameData = new ID3v2CommentFrameData(useFrameUnsynchronisation(), frame.getData());
                    if (!itunes && frameData.getDescription().toString().startsWith("iTun")) {
                        continue;
                    }
                    if (itunes && ITUNES_COMMENT_DESCRIPTION.equals(frameData.getDescription().toString())) {
                        return frameData;
                    } else if (!itunes) {
                        return frameData;
                    }
                } catch (InvalidDataException e) {
                    // Do nothing
                }
            }
        }
        return null;
    }

    private ID3v2PictureFrameData createPictureFrameData(String id) {
        ID3v2FrameSet frameSet = frameSets.get(id);
        if (frameSet != null) {
            ID3v2Frame frame = frameSet.getFrames().get(0);
            ID3v2PictureFrameData frameData;
            try {
                if (obseleteFormat)
                    frameData = new ID3v2ObseletePictureFrameData(useFrameUnsynchronisation(), frame.getData());
                else frameData = new ID3v2PictureFrameData(useFrameUnsynchronisation(), frame.getData());
                return frameData;
            } catch (InvalidDataException e) {
                // do nothing
            }
        }
        return null;
    }

    public boolean equals(Object obj) {
        if (!(obj instanceof AbstractID3v2Tag)) return false;
        if (super.equals(obj)) return true;
        AbstractID3v2Tag other = (AbstractID3v2Tag) obj;
        if (unsynchronisation != other.unsynchronisation) return false;
        if (extendedHeader != other.extendedHeader) return false;
        if (experimental != other.experimental) return false;
        if (footer != other.footer) return false;
        if (compression != other.compression) return false;
        if (dataLength != other.dataLength) return false;
        if (extendedHeaderLength != other.extendedHeaderLength) return false;
        if (version == null) {
            if (other.version != null) return false;
        } else if (other.version == null) return false;
        else if (!version.equals(other.version)) return false;
        if (frameSets == null && other.frameSets != null) {
            return false;
        } else if (other.frameSets == null) return false;
        else return frameSets.equals(other.frameSets);
    }
}
